/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2023. All rights reserved.
 *
 */

import fs from 'fs/promises';
import path from 'path';

import { max } from '@baize/wdk';
import { Json5Reader, PathUtil } from '@ohos/hvigor';
import fse from 'fs-extra';

import { QuickFixEnum } from '../enum/quick-fix-enum.js';
import { ProjectModelImpl } from '../model/project/project-model-impl.js';
import { DebugSymbol } from '../options/build/build-opt.js';
import { FileUtil } from '../utils/file-util.js';
import { getJson5Obj } from '../utils/json-util.js';
import { LibsFileCacheUtil, NativeLibsCache } from '../utils/libs-file-cache-util.js';
import { OhosLogger } from '../utils/log/ohos-logger.js';
import { TaskNames } from './common/task-names.js';
import { AssembleHqf } from './hook/assemble/assemble-hqf.js';
import { TargetTaskService } from './service/target-task-service.js';
import { getHqfName } from './sign/sign-util.js';
import { OhosHapTask } from './task/ohos-hap-task.js';

const logger = OhosLogger.getLogger('PrepareQuickfix');

export class PrepareQuickfix extends OhosHapTask {
  private readonly libsDir: string;
  private readonly debugSymbol?: DebugSymbol;
  private readonly libsCachePath: string;
  private readonly lastCachePath: string;
  private readonly patchConfig?: PatchConfig;

  constructor(taskService: TargetTaskService) {
    super(taskService, TaskNames.Task.PREPARE_QUICKFIX);
    this.debugSymbol = this.targetService.getNativeLibOption().debugSymbol;
    this.libsDir = this.pathInfo.getIntermediatesStrippedLibsDir();
    this.libsCachePath = path.resolve(this.pathInfo.getPatchDir(), 'base_native_libs.json');
    this.lastCachePath = path.resolve(this.pathInfo.getPatchDir(), 'libs.json');
    const buildConfigJsonPath = this.pathInfo.getBuildConfigPath();
    const buildConfig = getJson5Obj(buildConfigJsonPath);
    if (buildConfig) {
      this.patchConfig = buildConfig.patchConfig;
    }
  }

  protected async doTaskAction(): Promise<void> {
    // 处理native
    const changes = await this.collectNativeLibs();

    // 处理ets
    const [isEtsChanged, isImportNewFile] = await this.collectEts();

    // 处理资源
    await this.collectResources();

    await this.logQuickfixChanges(changes, isEtsChanged, isImportNewFile);

    await LibsFileCacheUtil.refreshLibsFileCache(this.pathInfo.getIntermediatesProcessLibs(), this.libsDir, this.lastCachePath, this.debugSymbol);

    // 生成patch.json
    await this.generatePatchJson();
  }

  async collectNativeLibs(): Promise<Record<string, number>> {
    const patchLibsDir: string = path.resolve(this.pathInfo.getPatchDir(), 'libs');
    await fse.emptydir(patchLibsDir);

    const baseCache: NativeLibsCache = fse.existsSync(this.libsCachePath) ? Json5Reader.getJson5Obj(this.libsCachePath) : { stripped: {}, libs: {} };

    const lastCache: NativeLibsCache = fse.existsSync(this.lastCachePath) ? Json5Reader.getJson5Obj(this.lastCachePath) : { stripped: {}, libs: {} };

    const current: Record<string, string> = fse.existsSync(this.libsDir) ? await LibsFileCacheUtil.generateFileHashesForDirectory(this.libsDir) : {};

    const changes = this.compareLibs(baseCache.stripped, lastCache.stripped, current);
    await fse.emptydir(patchLibsDir);

    const filter = (src: string) =>
      fse.statSync(src).isDirectory() || Object.entries(changes).some(([key, state]) => path.relative(src, key) === '' && state > QuickFixEnum.UNCHANGED);
    await fse.copy(this.libsDir, patchLibsDir, { filter, recursive: true, overwrite: true });
    return changes;
  }

  async collectResources(): Promise<void> {
    const patchResPath = path.resolve(this.pathInfo.getPatchDir(), 'resources');
    const patchResfilePath = path.resolve(patchResPath, 'resfile');
    const patchRawfilePath = path.resolve(patchResPath, 'rawfile');
    await fse.emptydir(patchResfilePath);
    await fse.emptydir(patchRawfilePath);
    if (this.patchConfig?.changedFileList && fse.existsSync(this.patchConfig.changedFileList)) {
      const changeFileListObj: ChangeFileListObj = getJson5Obj(this.patchConfig.changedFileList);

      await this.copyResources(patchResPath, changeFileListObj.resources?.resFile);

      await this.copyResources(patchResPath, changeFileListObj.resources?.rawFile);
    }
  }

  async copyResources(patchResPath: string, fileArr?: ModifiedResourcePath[]) {
    if (fileArr && fileArr?.length) {
      for (const { filePath, resourcePath } of fileArr) {
        if (path.basename(filePath) !== '.DS_Store' && fse.existsSync(filePath)) {
          const targetFilePath = path.join(patchResPath, path.relative(resourcePath, filePath));
          fse.copySync(filePath, targetFilePath);
        }
      }
    }
  }

  async collectEts(): Promise<boolean[]> {
    FileUtil.checkDirWithoutDelete(path.resolve(this.pathInfo.getPatchDir(), 'ets'));
    const patchMode = this.getPatchConfigMode();
    const etsCachePath =
      patchMode === 'hotReload'
        ? path.resolve(this.pathInfo.getInterMediatesLoaderOutPath(), 'ets', 'ets.json')
        : path.resolve(this.pathInfo.getPatchDir(), 'ets.json');
    let isEtsOrResFileChanged = false;
    let isImportNewFile = false;
    if (this.patchConfig?.changedFileList && fse.existsSync(this.patchConfig.changedFileList)) {
      const stats = fse.statSync(this.patchConfig.changedFileList);
      let etsCache: { changedFileList?: string; isImportNewFile?: boolean } = {};
      if (fse.existsSync(etsCachePath)) {
        etsCache = getJson5Obj(etsCachePath);
        isImportNewFile = etsCache.isImportNewFile ?? isImportNewFile;
      }

      // 判断changedFileList.json时间戳是否发生变化
      // 发生变化代表：arkts代码或者resfile资源发生了变化
      if (etsCache?.changedFileList !== String(stats.mtimeMs)) {
        logger.debug('changedFileList had been changed');
        etsCache.changedFileList = String(stats.mtimeMs);
        fse.writeJSONSync(etsCachePath, etsCache);
        isEtsOrResFileChanged = true;
        return [isEtsOrResFileChanged, isImportNewFile];
      }
    }
    return [isEtsOrResFileChanged, isImportNewFile];
  }

  private async logQuickfixChanges(changes: Record<string, number>, isEtsOrResFileChanged: boolean, isImportNewFile: boolean) {
    let state = max(Object.values(changes)) ?? QuickFixEnum.UNCHANGED;
    if (state !== QuickFixEnum.UNCHANGED) {
      logger.debug(`native files had been changed, state is ${state}`);
    }

    // arkTs、resfile变化都设置为2
    if (isEtsOrResFileChanged) {
      state = QuickFixEnum.LAST;
      logger.debug('arkTs files or resfile had been changed');
    }

    // 判断非cpp部分对比base是否有变化
    if (state === QuickFixEnum.UNCHANGED && this.patchConfig?.changedFileList && fse.existsSync(this.patchConfig.changedFileList)) {
      const changedFileList = getJson5Obj(this.patchConfig.changedFileList);
      state = this.isChanged(changedFileList) ? QuickFixEnum.BASE : state;
    }

    await fse.ensureDir(this.pathInfo.getModuleBuildOutputPath());
    await fse.writeJSON(path.resolve(this.pathInfo.getModuleBuildOutputPath(), 'quickfix.json'), {
      changes,
      state,
      isImportNewFile,
      file: getHqfName(this.moduleName, this.targetName, false),
    } as Quickfix);
  }

  compareLibs(base: Record<string, string>, last: Record<string, string>, current: Record<string, string>) {
    const diff: Record<string, number> = {};
    Object.entries(current).forEach(
      ([key, hash]) =>
        (diff[key] =
          !last[key] || last[key] === hash
            ? base[key] && base[key] === hash
              ? QuickFixEnum.UNCHANGED
              : last[key]
                ? QuickFixEnum.BASE
                : QuickFixEnum.LAST
            : QuickFixEnum.LAST),
    );
    return diff;
  }

  async generatePatchJson() {
    const patchVersionCode = await PrepareQuickfix.getCurrentHqfVersion();
    const appRes = (this.projectModel as ProjectModelImpl).getAppRes().getAppResOpt();
    const patchMode = this.getPatchConfigMode();
    let patch: HotReloadPatchJson | PatchJson;
    if (patchMode === 'hotReload') {
      patch = {
        app: {
          bundleName: appRes.app.bundleName,
          patchVersionCode, // 补丁版本号
          versionCode: appRes.app.versionCode, // 应用版本号
        },
        module: {
          name: this.moduleName,
          type: 'hotreload',
        },
      } as HotReloadPatchJson;
    } else {
      patch = {
        app: {
          bundleName: appRes.app.bundleName,
          versionCode: appRes.app.versionCode, // 应用版本号
          versionName: appRes.app.versionName,
          patchVersionCode, // 补丁版本号
          patchVersionName: String(patchVersionCode),
        },
        module: {
          name: this.moduleName,
          type: 'patch',
          deviceTypes: this.moduleModel.getDeviceTypes(),
          originalModuleHash: '', // 无校验, 可为空
        },
      } as PatchJson;
    }
    const hotReloadPatchPath = path.resolve(this.pathInfo.getModuleBuildIntermediates(), 'hotReload');
    const patchJsonDirPath = this.getPatchConfigMode() !== 'hotReload' ? this.pathInfo.getPatchDir() : hotReloadPatchPath;
    try {
      await fs.access(patchJsonDirPath);
    } catch (err) {
      await fs.mkdir(patchJsonDirPath);
    }
    await fs.writeFile(path.resolve(patchJsonDirPath, 'patch.json'), JSON.stringify(patch));
    logger.debug(`Prepare ${appRes.app.bundleName} quickfix with patch versionCode ${patchVersionCode}.`);
  }

  initTaskDepends(): void {
    if (this.getPatchConfigMode() !== 'hotReload') {
      this.declareDepends('ColdReloadArkTS');
      this.declareDepends(TaskNames.Task.DO_NATIVE_STRIP.name);
    }
  }

  /**
   * 用于根据buildConfig文件里的mode判断重载类型
   * 值：hotReload coldReload
   */
  getPatchConfigMode() {
    const buildConfigPath = this.pathInfo.getBuildConfigPath();
    if (fse.existsSync(buildConfigPath)) {
      const buildConfig = fse.readJsonSync(buildConfigPath);
      return buildConfig?.patchConfig?.mode;
    } else {
      logger.error(`${buildConfigPath} is not exists`);
    }
  }

  /**
   * obj里存在长度大于1的数组就返回true
   * @param {string[] | object} obj
   * @returns {boolean}
   */
  isChanged(obj: string[] | object): boolean {
    if (Array.isArray(obj)) {
      return obj.length > 0;
    } else if (typeof obj === 'object') {
      for (const e of Array.from(Object.values(obj))) {
        if (this.isChanged(e)) {
          return true;
        }
      }
    }
    return false;
  }

  static async getCurrentHqfVersion() {
    const logFile: string = path.resolve(PathUtil.getHvigorCacheDir(logger), 'outputs', 'patch', 'patch.json');
    if (!fse.existsSync(logFile)) {
      AssembleHqf.versionCode = 3000000;
      return AssembleHqf.versionCode;
    }
    if (AssembleHqf.versionCode) {
      return AssembleHqf.versionCode;
    }
    const quickfixLog = await fse.readJSON(logFile);
    AssembleHqf.versionCode = parseInt(quickfixLog?.patchVersionCode ? quickfixLog.patchVersionCode : 3000000);
    return AssembleHqf.versionCode;
  }
}

export interface PatchJson {
  app: PatchApp;
  module: PatchModule;
}

export interface HotReloadPatchJson {
  app: HotReloadPatchApp;
  module: HotReloadPatchModule;
}

export interface PatchApp {
  bundleName: string;
  versionCode: number; // 应用版本号
  versionName: string;
  patchVersionCode: number; // 补丁版本号
  patchVersionName: string;
}

export interface PatchModule {
  name: string;
  type: string;
  deviceTypes: string[];
  originalModuleHash: string; // 无校验, 可为空
}

export interface HotReloadPatchApp {
  bundleName: string;
  patchVersionCode: number; // 补丁版本号
  versionCode: number; // 应用版本号
}

export interface HotReloadPatchModule {
  name: string;
  type: string;
}

export interface Quickfix {
  changes: Record<string, number>;
  state: number;
  file: string;
}

export interface ChangeFileListObj {
  resources?: ResourcesObj;
}

interface ModifiedResourcePath {
  filePath: string;
  resourcePath: string;
}

export interface ResourcesObj {
  resFile?: ModifiedResourcePath[];
  rawFile?: ModifiedResourcePath[];
}

interface PatchConfig {
  enableMap: string;
  mode: string;
  changedFileList: string;
  oldMapFilePath: string;
  patchAbcPath: string;
}
